package org.yaxim.androidclient;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import org.jivesoftware.smackx.muc.MultiUserChat;
import org.yaxim.androidclient.chat.MUCChatWindow;
import org.yaxim.androidclient.chat.XMPPChatServiceAdapter;
import org.yaxim.androidclient.data.ChatHelper;
import org.yaxim.androidclient.data.ChatProvider;
import org.yaxim.androidclient.data.ChatProvider.ChatConstants;
import org.yaxim.androidclient.data.ChatRoomHelper;
import org.yaxim.androidclient.data.RosterProvider;
import org.yaxim.androidclient.data.RosterProvider.RosterConstants;
import org.yaxim.androidclient.data.YaximConfiguration;
import org.yaxim.androidclient.dialogs.AddRosterItemDialog;
import org.yaxim.androidclient.dialogs.ChangeStatusDialog;
import org.yaxim.androidclient.dialogs.ConfirmDialog;
import org.yaxim.androidclient.dialogs.EditMUCDialog;
import org.yaxim.androidclient.dialogs.FirstStartDialog;
import org.yaxim.androidclient.dialogs.GroupNameView;
import org.yaxim.androidclient.preferences.AccountPrefs;
import org.yaxim.androidclient.preferences.MainPrefs;
import org.yaxim.androidclient.service.IXMPPChatService;
import org.yaxim.androidclient.service.IXMPPMucService;
import org.yaxim.androidclient.service.XMPPService;
import org.yaxim.androidclient.util.ConnectionState;
import org.yaxim.androidclient.util.PreferenceConstants;
import org.yaxim.androidclient.util.StatusMode;
import org.yaxim.androidclient.util.XMPPHelper;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentValues;
import android.app.AlertDialog.Builder;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.content.ComponentName;
import android.content.DialogInterface.OnClickListener;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.DialogInterface;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.opengl.Visibility;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.util.Log;
import android.util.TypedValue;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.View;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ExpandableListView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;
import org.yaxim.androidclient.util.SimpleCursorTreeAdapter;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.ExpandableListView.ExpandableListContextMenuInfo;
import org.yaxim.androidclient.IXMPPRosterCallback;
import org.yaxim.androidclient.R;
import org.yaxim.androidclient.IXMPPRosterCallback.Stub;
import org.yaxim.androidclient.service.IXMPPRosterService;
import com.actionbarsherlock.app.ActionBar;
import com.actionbarsherlock.app.SherlockExpandableListActivity;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.Window;
import com.nullwire.trace.ExceptionHandler;
public class MainWindow extends SherlockExpandableListActivity {
private static final String TAG = "yaxim.MainWindow";
private YaximConfiguration mConfig;
private Handler mainHandler = new Handler();
private Intent xmppServiceIntent;
private ServiceConnection xmppServiceConnection;
private XMPPRosterServiceAdapter serviceAdapter;
private Stub rosterCallback;
private RosterExpListAdapter rosterListAdapter;
private TextView mConnectingText;
private FirstStartDialog mFirstStartDialog;
private ContentObserver mRosterObserver = new RosterObserver();
private ContentObserver mChatObserver = new ChatObserver();
private HashMap<String, Boolean> mGroupsExpanded = new HashMap<String, Boolean>();
private ActionBar actionBar;
private String mTheme;
private boolean mHandledIntent = false;
@Override
public void onCreate(Bundle savedInstanceState) {
Log.i(TAG, getString(R.string.build_version));
mConfig = YaximApplication.getConfig(this);
mTheme = mConfig.theme;
setTheme(mConfig.getTheme());
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_ACTION_BAR);
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
actionBar = getSupportActionBar();
actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_TITLE, ActionBar.DISPLAY_SHOW_TITLE);
actionBar.setHomeButtonEnabled(true);
registerCrashReporter();
getContentResolver().registerContentObserver(RosterProvider.CONTENT_URI,
true, mRosterObserver);
getContentResolver().registerContentObserver(ChatProvider.CONTENT_URI,
true, mChatObserver);
registerXMPPService();
createUICallback();
setupContenView();
registerListAdapter();
mHandledIntent = (getIntent().getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY) != 0;
}
@Override
public void onDestroy() {
super.onDestroy();
getContentResolver().unregisterContentObserver(mRosterObserver);
getContentResolver().unregisterContentObserver(mChatObserver);
}
public int getStatusActionIcon() {
boolean showOffline = !isConnected() || isConnecting()
|| getStatusMode() == null;
if (showOffline) {
return StatusMode.offline.getDrawableId();
}
return getStatusMode().getDrawableId();
}
// need this to workaround unwanted OnGroupCollapse/Expand events
boolean groupClicked = false;
void handleGroupChange(int groupPosition, boolean isExpanded) {
if (groupClicked) {
try {
String groupName = getGroupName(groupPosition);
Log.d(TAG, "group status change: " + groupName + " -> " + isExpanded);
mGroupsExpanded.put(groupName, isExpanded);
} catch (NullPointerException e) {
// sometimes, it fails to obtain the cursor. We can ignore it
}
groupClicked = false;
}
}
void setupContenView() {
setContentView(R.layout.main);
mConnectingText = (TextView)findViewById(R.id.error_view);
registerForContextMenu(getExpandableListView());
getExpandableListView().requestFocus();
getExpandableListView().setOnGroupClickListener(
new ExpandableListView.OnGroupClickListener() {
public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition,
long id) {
groupClicked = true;
return false;
}
});
getExpandableListView().setOnGroupCollapseListener(
new ExpandableListView.OnGroupCollapseListener() {
public void onGroupCollapse(int groupPosition) {
handleGroupChange(groupPosition, false);
}
});
getExpandableListView().setOnGroupExpandListener(
new ExpandableListView.OnGroupExpandListener() {
public void onGroupExpand(int groupPosition) {
handleGroupChange(groupPosition, true);
}
});
}
@Override
protected void onNewIntent(Intent i) {
setIntent(i);
mHandledIntent = false;
}
@Override
protected void onPause() {
super.onPause();
if (serviceAdapter != null)
serviceAdapter.unregisterUICallback(rosterCallback);
YaximApplication.getApp(this).mMTM.unbindDisplayActivity(this);
unbindXMPPService();
storeExpandedState();
}
@Override
protected void onResume() {
super.onResume();
if (mConfig.theme.equals(mTheme) == false) {
// restart
Intent restartIntent = new Intent(this, MainWindow.class);
restartIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(restartIntent);
finish();
}
showFirstStartUpDialogIfPrefsEmpty();
displayOwnStatus();
bindXMPPService();
YaximApplication.getApp(this).mMTM.bindDisplayActivity(this);
// handle SEND action
handleSendIntent();
}
public void handleSendIntent() {
Intent intent = getIntent();
String action = intent.getAction();
if (!mHandledIntent && (action != null) && (action.equals(Intent.ACTION_SEND))) {
showToastNotification(R.string.chooseContact);
setTitle(R.string.chooseContact);
}
}
public boolean openChatWithJid(String jid, String text) {
Log.d(TAG, "openChatWithJid: " + jid);
List<String[]> contacts = ChatHelper.getRosterContacts(this, ChatHelper.ROSTER_FILTER_ALL);
for (String[] c : contacts) {
if (jid.equalsIgnoreCase(c[0])) {
// found it
ChatHelper.startChatActivity(this, c[0], c[1], text);
finish();
return true;
}
}
// if we have a message, open chat to JID
if (text != null) {
ChatHelper.startChatActivity(this, jid, jid, text);
finish();
return true;
}
return false;
}
public boolean isJabberIntentAction(String action) {
return Intent.ACTION_VIEW.equals(action) ||
android.nfc.NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action);
}
public boolean transmogrifyXmppUri(Intent intent) {
Uri data = intent.getData();
if ("xmpp".equalsIgnoreCase(data.getScheme())) {
if (data.isOpaque()) {
// cheat around android's unwillingness to parse opaque URIs
data = Uri.parse(data.toString().replaceFirst(":", "://").replace(';', '&'));
}
} else if ("yax.im".equalsIgnoreCase(data.getHost())) {
// convert URI fragment (after # sign) into xmpp URI
String jid = data.getFragment().replace(';', '&');
data = Uri.parse("xmpp://" + XMPPHelper.jid2url(jid));
} else if ("conversations.im".equalsIgnoreCase(data.getHost())) {
try {
List<String> segments = data.getPathSegments();
String code = segments.get(0);
String jid = segments.get(1);
String token = "";
if (!jid.contains("@")) {
jid = segments.get(1) + "@" + segments.remove(2);
}
if (segments.size() > 2)
token = "&preauth=" + segments.get(2);
if ("i".equalsIgnoreCase(code))
data = Uri.parse("xmpp://" + jid + "?roster" + token);
else if ("j".equalsIgnoreCase(code))
data = Uri.parse("xmpp://" + jid + "?join");
else return false;
} catch (Exception e) {
Log.d(TAG, "Failed to parse URI " + data);
return false;
}
} else
return false;
Log.d(TAG, "transmogrifyXmppUri: " + intent.getData() + " --> " + data);
intent.setData(data);
return true;
}
public void handleJabberIntent() {
Intent intent = getIntent();
Log.d(TAG, "handleJabberIntent: " + intent);
String action = intent.getAction();
Uri data = intent.getData();
if (action == null || data == null || mHandledIntent)
return;
if (action.equals(Intent.ACTION_SENDTO) && data.getHost().equals("jabber")) {
// 1. look for JID in roster; 2. attempt to add
String jid = data.getPathSegments().get(0);
if (!openChatWithJid(jid, null) &&
!addToRosterDialog(jid))
finish();
} else if (isJabberIntentAction(action) && transmogrifyXmppUri(intent)) {
data = intent.getData();
String jid = data.getAuthority();
String body = data.getQueryParameter("body");
String name = data.getQueryParameter("name");
String preauth = data.getQueryParameter("preauth");
if (data.getQueryParameter("roster") != null || data.getQueryParameter("subscribe") != null) {
addToRosterDialog(jid, name, preauth);
} else if (data.getQueryParameter("join") != null && !openChatWithJid(jid, null)) {
new EditMUCDialog(this, jid, data.getQueryParameter("body"),
null, data.getQueryParameter("password")).withNick(mConfig.userName).show();
} else if (!openChatWithJid(jid, body) &&
!addToRosterDialog(jid, name, preauth)) {
finish();
} else return;
} else return;
// clear the intent data to prevent re-triggering
getIntent().setData(null);
mHandledIntent = true;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Log.d(TAG, "onConfigurationChanged");
getExpandableListView().requestFocus();
}
private boolean isConnected() {
return serviceAdapter != null && serviceAdapter.isAuthenticated();
}
private boolean isConnecting() {
return serviceAdapter != null &&
(serviceAdapter.getConnectionState() == ConnectionState.CONNECTING ||
serviceAdapter.getConnectionState() == ConnectionState.LOADING);
}
public void updateRoster() {
loadUnreadCounters();
rosterListAdapter.requery();
restoreGroupsExpanded();
}
private StatusMode getContactStatusMode(Cursor c) {
try {
return StatusMode.values()[c.getInt(c.getColumnIndexOrThrow(RosterConstants.STATUS_MODE))];
} catch (Exception e) {
Log.e(TAG, "Invalid status for contact " + e.getMessage());
return StatusMode.unknown;
}
}
private StatusMode getItemStatusMode(long packedPosition) {
int flatPosition = getExpandableListView().getFlatListPosition(packedPosition);
Cursor c = (Cursor)getExpandableListView().getItemAtPosition(flatPosition);
return getContactStatusMode(c);
}
private String getPackedItemRow(long packedPosition, String rowName) {
int flatPosition = getExpandableListView().getFlatListPosition(packedPosition);
Cursor c = (Cursor)getExpandableListView().getItemAtPosition(flatPosition);
return c.getString(c.getColumnIndex(rowName));
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenu.ContextMenuInfo menuInfo) {
ExpandableListView.ExpandableListContextMenuInfo info;
try {
info = (ExpandableListView.ExpandableListContextMenuInfo) menuInfo;
} catch (ClassCastException e) {
Log.e(TAG, "bad menuinfo: ", e);
return;
}
long packedPosition = info.packedPosition;
boolean isChild = isChild(packedPosition);
// get the entry name for the item
String menuName;
boolean isMuc=false;
if (isChild) {
// do not show context menu before a contact has been added
if (getItemStatusMode(packedPosition) == StatusMode.subscribe)
return;
getMenuInflater().inflate(R.menu.roster_item_contextmenu, menu);
menuName = String.format("%s (%s)",
getPackedItemRow(packedPosition, RosterConstants.ALIAS),
getPackedItemRow(packedPosition, RosterConstants.JID));
isMuc = ChatRoomHelper.isRoom(this, getPackedItemRow(packedPosition, RosterConstants.JID));
if (isMuc) {
getMenuInflater().inflate(R.menu.muc_options, menu);
menu.findItem(R.id.chat_optionsmenu_userlist).setVisible(false);
} else
getMenuInflater().inflate(R.menu.contact_options, menu);
} else {
menuName = getPackedItemRow(packedPosition, RosterConstants.GROUP);
if (menuName.equals("") || menuName.equals(RosterConstants.MUCS))
return; // no options for default menu
getMenuInflater().inflate(R.menu.roster_group_contextmenu, menu);
}
menu.setHeaderTitle(menuName);
}
void removeChatHistory(final String JID) {
getContentResolver().delete(ChatProvider.CONTENT_URI,
ChatProvider.ChatConstants.JID + " = ?", new String[] { JID });
}
void removeRosterItemDialog(final String JID, final String userName) {
new AlertDialog.Builder(this)
.setTitle(R.string.deleteRosterItem_title)
.setMessage(getString(R.string.deleteRosterItem_text, userName, JID))
.setPositiveButton(android.R.string.yes,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
serviceAdapter.removeRosterItem(JID);
}
})
.setNegativeButton(android.R.string.no, null)
.create().show();
}
boolean addToRosterDialog(String jid, String alias, String token) {
if (serviceAdapter != null && serviceAdapter.isAuthenticated()) {
new AddRosterItemDialog(this, serviceAdapter, jid)
.setAlias(alias)
.setToken(token)
.show();
return true;
} else {
showToastNotification(R.string.Global_authenticate_first);
return false;
}
}
boolean addToRosterDialog(String jid) {
return addToRosterDialog(jid, null, null);
}
void rosterAddRequestedDialog(final String jid, final String alias, String message) {
new AlertDialog.Builder(this)
.setTitle(R.string.subscriptionRequest_title)
.setMessage(getString(R.string.subscriptionRequest_text, alias,
message != null ? message : ""))
.setPositiveButton(R.string.subscription_accept,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
serviceAdapter.sendPresenceRequest(jid, "subscribed");
// show dialog if not yet configured
if (alias.equals(jid))
addToRosterDialog(jid);
}
})
.setNegativeButton(R.string.subscription_reject,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
serviceAdapter.sendPresenceRequest(jid, "unsubscribed");
}
})
.create().show();
}
abstract class EditOk {
abstract public void ok(String result);
}
void editTextDialog(int titleId, CharSequence message, String text,
final EditOk ok) {
LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
View layout = inflater.inflate(R.layout.edittext_dialog,
(ViewGroup) findViewById(R.id.layout_root));
TextView messageView = (TextView) layout.findViewById(R.id.text);
messageView.setText(message);
final EditText input = (EditText) layout.findViewById(R.id.editText);
input.setTransformationMethod(android.text.method.SingleLineTransformationMethod.getInstance());
input.setText(text);
new AlertDialog.Builder(this)
.setTitle(titleId)
.setView(layout)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
String newName = input.getText().toString();
if (newName.length() != 0)
ok.ok(newName);
}})
.setNegativeButton(android.R.string.cancel, null)
.create().show();
}
void renameRosterItemDialog(final String JID, final String userName) {
String newUserName = userName;
if (JID.equals(userName))
newUserName = XMPPHelper.capitalizeString(JID.split("@")[0]);
editTextDialog(R.string.RenameEntry_title,
getString(R.string.RenameEntry_summ, userName, JID),
newUserName, new EditOk() {
public void ok(String result) {
serviceAdapter.renameRosterItem(JID, result);
}
});
}
void renameRosterGroupDialog(final String groupName) {
editTextDialog(R.string.RenameGroup_title,
getString(R.string.RenameGroup_summ, groupName),
groupName, new EditOk() {
public void ok(String result) {
serviceAdapter.renameRosterGroup(groupName, result);
}
});
}
void moveRosterItemToGroupDialog(final String jabberID) {
LayoutInflater inflater = (LayoutInflater)getSystemService(
LAYOUT_INFLATER_SERVICE);
View group = inflater.inflate(R.layout.moverosterentrytogroupview, null, false);
final GroupNameView gv = (GroupNameView)group.findViewById(R.id.moverosterentrytogroupview_gv);
gv.setGroupList(getRosterGroups());
new AlertDialog.Builder(this)
.setTitle(R.string.MoveRosterEntryToGroupDialog_title)
.setView(group)
.setPositiveButton(android.R.string.ok,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
Log.d(TAG, "new group: " + gv.getGroupName());
serviceAdapter.moveRosterItemToGroup(jabberID,
gv.getGroupName());
}
})
.setNegativeButton(android.R.string.cancel, null)
.create().show();
}
public boolean onContextItemSelected(MenuItem item) {
return applyMenuContextChoice(item);
}
private boolean applyMenuContextChoice(MenuItem item) {
ExpandableListContextMenuInfo contextMenuInfo = (ExpandableListContextMenuInfo) item
.getMenuInfo();
long packedPosition = contextMenuInfo.packedPosition;
if (isChild(packedPosition)) {
String userJid = getPackedItemRow(packedPosition, RosterConstants.JID);
String userName = getPackedItemRow(packedPosition, RosterConstants.ALIAS);
Log.d(TAG, "action for contact " + userName + "/" + userJid);
int itemID = item.getItemId();
switch (itemID) {
case R.id.roster_contextmenu_contact_delete:
if (!isConnected()) { showToastNotification(R.string.Global_authenticate_first); return true; }
removeRosterItemDialog(userJid, userName);
return true;
case R.id.roster_contextmenu_contact_rename:
if (!isConnected()) { showToastNotification(R.string.Global_authenticate_first); return true; }
renameRosterItemDialog(userJid, userName);
return true;
case R.id.roster_contextmenu_contact_request_auth:
if (!isConnected()) { showToastNotification(R.string.Global_authenticate_first); return true; }
serviceAdapter.sendPresenceRequest(userJid, "subscribe");
return true;
case R.id.roster_contextmenu_contact_change_group:
if (!isConnected()) { showToastNotification(R.string.Global_authenticate_first); return true; }
moveRosterItemToGroupDialog(userJid);
return true;
default:
return ChatHelper.handleJidOptions(this, itemID, userJid, userName);
}
} else {
int itemID = item.getItemId();
String seletedGroup = getPackedItemRow(packedPosition, RosterConstants.GROUP);
Log.d(TAG, "action for group " + seletedGroup);
switch (itemID) {
case R.id.roster_contextmenu_group_rename:
if (!isConnected()) { showToastNotification(R.string.Global_authenticate_first); return true; }
renameRosterGroupDialog(seletedGroup);
return true;
}
}
return false;
}
private boolean isChild(long packedPosition) {
int type = ExpandableListView.getPackedPositionType(packedPosition);
return (type == ExpandableListView.PACKED_POSITION_TYPE_CHILD);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getSupportMenuInflater().inflate(R.menu.roster_options, menu);
return true;
}
void setMenuItem(Menu menu, int itemId, int iconId, CharSequence title) {
com.actionbarsherlock.view.MenuItem item = menu.findItem(itemId);
if (item == null)
return;
item.setIcon(iconId);
item.setTitle(title);
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
setMenuItem(menu, R.id.menu_connect, getConnectDisconnectIcon(),
getConnectDisconnectText());
setMenuItem(menu, R.id.menu_show_hide, getShowHideMenuIcon(),
getShowHideMenuText());
return true;
}
@Override
public boolean onOptionsItemSelected(com.actionbarsherlock.view.MenuItem item) {
return applyMainMenuChoice(item);
}
private int getShowHideMenuIcon() {
TypedValue tv = new TypedValue();
if (mConfig.showOffline) {
getTheme().resolveAttribute(R.attr.OnlineFriends, tv, true);
return tv.resourceId;
}
getTheme().resolveAttribute(R.attr.AllFriends, tv, true);
return tv.resourceId;
}
private String getShowHideMenuText() {
return mConfig.showOffline ? getString(R.string.Menu_HideOff)
: getString(R.string.Menu_ShowOff);
}
public StatusMode getStatusMode() {
return mConfig.getPresenceMode();
}
public void updateStatus(StatusMode statusMode) {
displayOwnStatus();
if (serviceAdapter == null)
return; // we can't do anything, let's pray service will update from config
// check if we are connected and want to go offline
boolean needToDisconnect = (statusMode == StatusMode.offline) && isConnected();
// check if we want to reconnect
boolean needToConnect = (statusMode != StatusMode.offline) &&
serviceAdapter.getConnectionState() == ConnectionState.OFFLINE;
if (needToConnect || needToDisconnect)
toggleConnection();
else if (isConnected())
serviceAdapter.setStatusFromConfig();
}
private void displayOwnStatus() {
// This and many other things like it should be done with observer
actionBar.setIcon(getStatusActionIcon());
if (mConfig.statusMessage.equals("")) {
actionBar.setSubtitle(null);
} else {
actionBar.setSubtitle(mConfig.statusMessage);
}
}
private void aboutDialog() {
LayoutInflater inflater = (LayoutInflater)getSystemService(
LAYOUT_INFLATER_SERVICE);
View about = inflater.inflate(R.layout.aboutview, null, false);
String versionTitle = getString(R.string.AboutDialog_title);
try {
PackageInfo pi = getPackageManager()
.getPackageInfo(getPackageName(), 0);
versionTitle += " v" + pi.versionName;
} catch (NameNotFoundException e) {
}
// fix translator-credits: hide if unset, format otherwise
TextView tcv = (TextView)about.findViewById(R.id.translator_credits);
if (tcv.getText().equals("translator-credits"))
tcv.setVisibility(View.GONE);
new AlertDialog.Builder(this)
.setTitle(versionTitle)
.setIcon(android.R.drawable.ic_dialog_info)
.setView(about)
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(R.string.AboutDialog_Vote, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int item) {
Intent market = new Intent(Intent.ACTION_VIEW,
Uri.parse("market://details?id=" + getPackageName()));
market.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY | Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
try {
startActivity(market);
} catch (Exception e) {
// do not crash
Log.e(TAG, "could not go to market: " + e);
}
}
})
.create().show();
}
private boolean applyMainMenuChoice(com.actionbarsherlock.view.MenuItem item) {
int itemID = item.getItemId();
switch (itemID) {
case R.id.menu_connect:
toggleConnection();
return true;
case R.id.menu_add_friend:
addToRosterDialog(null);
return true;
case R.id.menu_show_hide:
setOfflinceContactsVisibility(!mConfig.showOffline);
updateRoster();
return true;
case R.id.menu_markallread:
ChatHelper.markAllAsRead(this);
return true;
case android.R.id.home:
new ChangeStatusDialog(this, mConfig).show();
return true;
case R.id.menu_exit:
PreferenceManager.getDefaultSharedPreferences(this).edit().
putBoolean(PreferenceConstants.CONN_STARTUP, false).commit();
stopService(xmppServiceIntent);
finish();
return true;
case R.id.menu_settings:
startActivity(new Intent(this, MainPrefs.class));
return true;
case R.id.menu_about:
aboutDialog();
return true;
case R.id.menu_muc:
new EditMUCDialog(this).withNick(mConfig.userName).show();
return true;
case R.id.menu_send_invitation:
XMPPHelper.shareLink(this, R.string.Menu_send_invitation,
XMPPHelper.createInvitationLinkHTTPS(mConfig.jabberID,
mConfig.createInvitationCode()));
return true;
}
return false;
}
/** Sets if all contacts are shown in the roster or online contacts only. */
@TargetApi(Build.VERSION_CODES.HONEYCOMB) // required for Sherlock's invalidateOptionsMenu */
private void setOfflinceContactsVisibility(boolean showOffline) {
PreferenceManager.getDefaultSharedPreferences(this).edit().
putBoolean(PreferenceConstants.SHOW_OFFLINE, showOffline).commit();
invalidateOptionsMenu();
}
@Override
public boolean onChildClick(ExpandableListView parent, View v,
int groupPosition, int childPosition, long id) {
long packedPosition = ExpandableListView.getPackedPositionForChild(groupPosition, childPosition);
Cursor c = (Cursor)getExpandableListView().getItemAtPosition(getExpandableListView().getFlatListPosition(packedPosition));
String userJid = c.getString(c.getColumnIndexOrThrow(RosterConstants.JID));
String userName = c.getString(c.getColumnIndexOrThrow(RosterConstants.ALIAS));
Intent i = getIntent();
if (!mHandledIntent && i.getAction() != null && i.getAction().equals(Intent.ACTION_SEND)) {
// delegate ACTION_SEND to child window and close self
ChatHelper.startChatActivity(this, userJid, userName, i.getStringExtra(Intent.EXTRA_TEXT));
finish();
} else {
StatusMode s = getContactStatusMode(c);
if (s == StatusMode.subscribe)
rosterAddRequestedDialog(userJid, userName,
c.getString(c.getColumnIndexOrThrow(RosterConstants.STATUS_MESSAGE)));
else
ChatHelper.startChatActivity(this, userJid, userName, null);
}
return true;
}
private void updateConnectionState(ConnectionState cs) {
Log.d(TAG, "updateConnectionState: " + cs);
displayOwnStatus();
boolean spinTheSpinner = false;
switch (cs) {
case CONNECTING:
case LOADING:
case DISCONNECTING:
spinTheSpinner = true;
case DISCONNECTED:
case RECONNECT_NETWORK:
case RECONNECT_DELAYED:
case OFFLINE:
if (cs == ConnectionState.DISCONNECTED && PreferenceManager.getDefaultSharedPreferences(this)
.contains(PreferenceConstants.INITIAL_CREATE)) {
// somehow, cs==OFFLINE is triggered twice, but cs==DISCONNECTED only once
String error = serviceAdapter.getConnectionStateString().replace("conflict(-1) ", "");
if (error.contains("\n")) // TODO: work around getConnectionStateString() returning two lines
error = error.split("\n")[1];
if (error.contains("SASL authentication failed")) // TODO: hack to circumvent old smack
error = getString(R.string.StartupDialog_auth_failed);
Toast.makeText(this, error, Toast.LENGTH_SHORT).show();
showFirstStartUpDialog();
} else
if (cs == ConnectionState.OFFLINE) // override with "Offline" string, no error message
mConnectingText.setText(R.string.conn_offline);
else
mConnectingText.setText(serviceAdapter.getConnectionStateString());
mConnectingText.setVisibility(View.VISIBLE);
setSupportProgressBarIndeterminateVisibility(spinTheSpinner);
break;
case ONLINE:
mConnectingText.setVisibility(View.GONE);
setSupportProgressBarIndeterminateVisibility(false);
PreferenceManager.getDefaultSharedPreferences(this).edit().
remove(PreferenceConstants.INITIAL_CREATE).commit();
}
}
public void startConnection(boolean create_account) {
xmppServiceIntent.putExtra("create_account", create_account);
startService(xmppServiceIntent);
}
// this function changes the prefs to keep the connection
// according to the requested state
private void toggleConnection() {
if (!mConfig.jid_configured) {
startActivity(new Intent(this, AccountPrefs.class));
return;
}
boolean oldState = isConnected() || isConnecting();
PreferenceManager.getDefaultSharedPreferences(this).edit().
putBoolean(PreferenceConstants.CONN_STARTUP, !oldState).commit();
if (oldState) {
serviceAdapter.disconnect();
stopService(xmppServiceIntent);
} else
startConnection(false);
}
private int getConnectDisconnectIcon() {
if (isConnected() || isConnecting()) {
return R.drawable.ic_menu_unplug;
}
return R.drawable.ic_menu_plug;
}
private String getConnectDisconnectText() {
if (isConnected() || isConnecting()) {
return getString(R.string.Menu_disconnect);
}
return getString(R.string.Menu_connect);
}
private void registerXMPPService() {
Log.i(TAG, "called startXMPPService()");
xmppServiceIntent = new Intent(this, XMPPService.class);
xmppServiceIntent.setAction("org.yaxim.androidclient.XMPPSERVICE");
xmppServiceConnection = new ServiceConnection() {
@TargetApi(Build.VERSION_CODES.HONEYCOMB) // required for Sherlock's invalidateOptionsMenu */
public void onServiceConnected(ComponentName name, IBinder service) {
Log.i(TAG, "called onServiceConnected()");
serviceAdapter = new XMPPRosterServiceAdapter(
IXMPPRosterService.Stub.asInterface(service));
serviceAdapter.registerUICallback(rosterCallback);
Log.i(TAG, "getConnectionState(): "
+ serviceAdapter.getConnectionState());
invalidateOptionsMenu(); // to load the action bar contents on time for access to icons/progressbar
ConnectionState cs = serviceAdapter.getConnectionState();
updateConnectionState(cs);
updateRoster();
// when returning from prefs to main activity, apply new config
if (mConfig.reconnect_required && cs == ConnectionState.ONLINE) {
// login config changed, force reconnection
serviceAdapter.disconnect();
serviceAdapter.connect();
} else if (mConfig.presence_required && isConnected())
serviceAdapter.setStatusFromConfig();
// handle server-related intents after connecting to the backend
handleJabberIntent();
}
public void onServiceDisconnected(ComponentName name) {
Log.i(TAG, "called onServiceDisconnected()");
}
};
}
private void unbindXMPPService() {
try {
unbindService(xmppServiceConnection);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Service wasn't bound!");
}
}
private void bindXMPPService() {
bindService(xmppServiceIntent, xmppServiceConnection, BIND_AUTO_CREATE);
}
private void registerListAdapter() {
rosterListAdapter = new RosterExpListAdapter(this);
setListAdapter(rosterListAdapter);
}
private void createUICallback() {
rosterCallback = new IXMPPRosterCallback.Stub() {
@Override
public void connectionStateChanged(final int connectionstate)
throws RemoteException {
mainHandler.post(new Runnable() {
@TargetApi(Build.VERSION_CODES.HONEYCOMB) // required for Sherlock's invalidateOptionsMenu */
public void run() {
ConnectionState cs = ConnectionState.values()[connectionstate];
//Log.d(TAG, "connectionStatusChanged: " + cs);
updateConnectionState(cs);
invalidateOptionsMenu();
}
});
}
};
}
// store mGroupsExpanded into prefs (this is a hack, but SQLite /
// content providers suck wrt. virtual groups)
public void storeExpandedState() {
SharedPreferences.Editor prefedit = PreferenceManager
.getDefaultSharedPreferences(this).edit();
for (HashMap.Entry<String, Boolean> item : mGroupsExpanded.entrySet()) {
prefedit.putBoolean("expanded_" + item.getKey(), item.getValue());
}
prefedit.commit();
}
// get the name of a roster group from the cursor
public String getGroupName(int groupId) {
// default group is "" and MUC group is "\uFFFF"
return java.net.URLEncoder.encode(getPackedItemRow(
ExpandableListView.getPackedPositionForGroup(groupId),
RosterConstants.GROUP));
}
public void restoreGroupsExpanded() {
SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(this);
for (int count = 0; count < getExpandableListAdapter().getGroupCount(); count++) {
String name = getGroupName(count);
if (!mGroupsExpanded.containsKey(name))
mGroupsExpanded.put(name, prefs.getBoolean("expanded_" + name, true));
if (mGroupsExpanded.get(name))
getExpandableListView().expandGroup(count);
else
getExpandableListView().collapseGroup(count);
}
}
private void showFirstStartUpDialog() {
if (mFirstStartDialog == null)
mFirstStartDialog = new FirstStartDialog(this, serviceAdapter);
mFirstStartDialog.show();
}
private void showFirstStartUpDialogIfPrefsEmpty() {
Log.i(TAG, "showFirstStartUpDialogIfPrefsEmpty, JID: "
+ mConfig.jabberID);
SharedPreferences prefs = PreferenceManager
.getDefaultSharedPreferences(this);
if (mConfig.jabberID.length() < 3 || prefs.contains(PreferenceConstants.INITIAL_CREATE)) {
// load preference defaults
PreferenceManager.setDefaultValues(this, R.xml.mainprefs, false);
PreferenceManager.setDefaultValues(this, R.xml.accountprefs, false);
// prevent a start-up with empty JID
prefs.edit().putBoolean(PreferenceConstants.CONN_STARTUP, false).commit();
// show welcome dialog
showFirstStartUpDialog();
} else
XMPPHelper.setNFCInvitation(this, mConfig);
}
public static Intent createIntent(Context context) {
Intent i = new Intent(context, MainWindow.class);
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
return i;
}
protected void showToastNotification(int message) {
Toast tmptoast = Toast.makeText(this, message, Toast.LENGTH_SHORT);
tmptoast.show();
}
private void registerCrashReporter() {
if (mConfig.reportCrash) {
ExceptionHandler.register(this, "http://duenndns.de/yaxim-crash/");
}
}
private static final String OFFLINE_EXCLUSION =
RosterConstants.STATUS_MODE + " > " + StatusMode.offline.ordinal();
private static final String countAvailableMembers =
"SELECT COUNT() FROM " + RosterProvider.TABLE_ROSTER + " inner_query" +
" WHERE inner_query." + RosterConstants.GROUP + " = " +
RosterProvider.QUERY_ALIAS + "." + RosterConstants.GROUP +
" AND inner_query." + OFFLINE_EXCLUSION;
private static final String countMembers =
"SELECT COUNT() FROM " + RosterProvider.TABLE_ROSTER + " inner_query" +
" WHERE inner_query." + RosterConstants.GROUP + " = " +
RosterProvider.QUERY_ALIAS + "." + RosterConstants.GROUP;
private static final String[] GROUPS_QUERY = new String[] {
RosterConstants._ID,
RosterConstants.GROUP,
};
private static final String[] GROUPS_QUERY_COUNTED = new String[] {
RosterConstants._ID,
RosterConstants.GROUP,
"(" + countAvailableMembers + ") || '/' || (" + countMembers + ") AS members"
};
final String countAvailableMembersTotals =
"SELECT COUNT() FROM " + RosterProvider.TABLE_ROSTER + " inner_query" +
" WHERE inner_query." + OFFLINE_EXCLUSION;
final String countMembersTotals =
"SELECT COUNT() FROM " + RosterProvider.TABLE_ROSTER;
final String[] GROUPS_QUERY_CONTACTS_DISABLED = new String[] {
RosterConstants._ID,
"'' AS " + RosterConstants.GROUP,
"(" + countAvailableMembersTotals + ") || '/' || (" + countMembersTotals + ") AS members",
"MIN(" + RosterConstants._ID + ")" // cheat: aggregate function to only return a single entry
};
private static final String[] GROUPS_FROM = new String[] {
RosterConstants.GROUP,
"members"
};
private static final int[] GROUPS_TO = new int[] {
R.id.groupname,
R.id.members
};
private static final String[] ROSTER_QUERY = new String[] {
RosterConstants._ID,
RosterConstants.JID,
RosterConstants.ALIAS,
RosterConstants.STATUS_MODE,
RosterConstants.STATUS_MESSAGE,
};
public List<String> getRosterGroups() {
// we want all, online and offline
List<String> list = new ArrayList<String>();
Cursor cursor = getContentResolver().query(RosterProvider.GROUPS_URI, GROUPS_QUERY,
null, null, RosterConstants.GROUP);
int idx = cursor.getColumnIndex(RosterConstants.GROUP);
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
list.add(cursor.getString(idx));
cursor.moveToNext();
}
cursor.close();
list.remove(RosterProvider.RosterConstants.MUCS);
return list;
}
public class RosterExpListAdapter extends SimpleCursorTreeAdapter {
public RosterExpListAdapter(Context context) {
super(context, /* cursor = */ null,
R.layout.maingroup_row, GROUPS_FROM, GROUPS_TO,
R.layout.mainchild_row,
new String[] {
RosterConstants.ALIAS,
RosterConstants.STATUS_MESSAGE,
RosterConstants.STATUS_MODE
},
new int[] {
R.id.roster_screenname,
R.id.roster_statusmsg,
R.id.roster_icon
});
}
public void requery() {
String selectWhere = null;
if (!mConfig.showOffline)
selectWhere = OFFLINE_EXCLUSION;
Uri query_uri = RosterProvider.GROUPS_URI;
String[] query = GROUPS_QUERY_COUNTED;
if(!mConfig.enableGroups) {
query = GROUPS_QUERY_CONTACTS_DISABLED;
query_uri = RosterProvider.CONTENT_URI;
}
Cursor cursor = getContentResolver().query(query_uri,
query, selectWhere, null, RosterConstants.GROUP);
Cursor oldCursor = getCursor();
changeCursor(cursor);
if (oldCursor != null)
stopManagingCursor(oldCursor);
}
@Override
protected Cursor getChildrenCursor(Cursor groupCursor) {
// Given the group, we return a cursor for all the children within that group
String selectWhere;
int idx = groupCursor.getColumnIndex(RosterConstants.GROUP);
String groupname = groupCursor.getString(idx);
String[] args = null;
if(!mConfig.enableGroups) {
selectWhere = mConfig.showOffline ? "" : OFFLINE_EXCLUSION;
} else {
selectWhere = mConfig.showOffline ? "" : OFFLINE_EXCLUSION + " AND ";
selectWhere += RosterConstants.GROUP + " = ?";
args = new String[] { groupname };
}
return getContentResolver().query(RosterProvider.CONTENT_URI, ROSTER_QUERY,
selectWhere, args, null);
}
@Override
protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
super.bindGroupView(view, context, cursor, isExpanded);
if (cursor.getString(cursor.getColumnIndexOrThrow(RosterConstants.GROUP)).length() == 0) {
TextView groupname = (TextView)view.findViewById(R.id.groupname);
groupname.setText(mConfig.enableGroups ? R.string.default_group : R.string.all_contacts_group);
} else
if (cursor.getString(cursor.getColumnIndexOrThrow(RosterConstants.GROUP)).equals(RosterProvider.RosterConstants.MUCS)) {
((TextView)view.findViewById(R.id.groupname)).setText(R.string.muc_group);
}
}
@Override
protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
super.bindChildView(view, context, cursor, isLastChild);
TextView statusmsg = (TextView)view.findViewById(R.id.roster_statusmsg);
boolean hasStatus = statusmsg.getText() != null && statusmsg.getText().length() > 0;
statusmsg.setVisibility(hasStatus ? View.VISIBLE : View.GONE);
String jid = cursor.getString(cursor.getColumnIndex(RosterConstants.JID));
TextView unreadmsg = (TextView)view.findViewById(R.id.roster_unreadmsg_cnt);
Integer count = mUnreadCounters.get(jid);
if (count == null)
count = 0;
unreadmsg.setText(count.toString());
unreadmsg.setVisibility(count > 0 ? View.VISIBLE : View.GONE);
unreadmsg.bringToFront();
}
protected void setViewImage(ImageView v, String value) {
int presenceMode = Integer.parseInt(value);
v.setImageResource(getIconForPresenceMode(presenceMode));
}
private int getIconForPresenceMode(int presenceMode) {
if (!isConnected()) // override icon if we are offline
presenceMode = 0;
return StatusMode.values()[presenceMode].getDrawableId();
}
}
private class RosterObserver extends ContentObserver {
public RosterObserver() {
super(mainHandler);
}
public void onChange(boolean selfChange) {
Log.d(TAG, "RosterObserver.onChange: " + selfChange);
// work around race condition in ExpandableListView, which collapses
// groups rand-f**king-omly
if (getExpandableListAdapter() != null)
mainHandler.postDelayed(new Runnable() {
public void run() {
restoreGroupsExpanded();
}}, 100);
}
}
private HashMap<String, Integer> mUnreadCounters = new HashMap<String, Integer>();
private void loadUnreadCounters() {
final String[] PROJECTION = new String[] { ChatConstants.JID, "count(*)" };
final String SELECTION = ChatConstants.DIRECTION + " = " + ChatConstants.INCOMING + " AND " +
ChatConstants.DELIVERY_STATUS + " = " + ChatConstants.DS_NEW +
") GROUP BY (" + ChatConstants.JID; // hack!
Cursor c = getContentResolver().query(ChatProvider.CONTENT_URI,
PROJECTION, SELECTION, null, null);
mUnreadCounters.clear();
if(c!=null){
while (c.moveToNext())
mUnreadCounters.put(c.getString(0), c.getInt(1));
c.close();
}
}
private class ChatObserver extends ContentObserver {
public ChatObserver() {
super(mainHandler);
}
public void onChange(boolean selfChange) {
updateRoster();
}
}
}